package demo.java.net;

import org.junit.Test;

import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.stream.Stream;

/**
 * 域名你不陌生，IP地址你也不陌生，但域名解析或许你有些陌生。Java并不希望使用者了解过多的DNS相关知识，因此使用了InetAddress来完成域名 - IP地址的互转工作。
 * <p>
 * IP地址是IP使用的32位（IPv4）或者128位（IPv6）位无符号数字，它是传输层协议TCP，UDP的基础。
 * InetAddress是Java对IP地址的封装，几乎所有的Java网络相关的类都和它有关系，
 * 例如：serversocket,socket,URL,DataGramSocket,DataGRamPacket等。
 * <p>
 * InetAddress的实例对象包含以数字形式保存的IP地址，同时还可能包含主机名（如果使用主机名来获取InetAddress的实例，或者使用数字来构造，并且启用了反向主机名解析的功能）。
 * InetAddress类提供了将主机名解析为IP地址（或反之）的方法。
 * <p>
 * InetAddress对域名进行解析是使用本地机器配置或者网络命名服务 （如域名系统 （Domain Name
 * System，DNS）和网络信息服务（Network Information Service，NIS））来实现。
 * 对于DNS来说，本地需要向DNS服务器发送查询的请求，然后服务器根据一系列的操作，返回对应的IP地址，
 * 为了提高效率，通常本地会缓存一些主机名与IP地址的映射，这样访问相同的地址，就不需要重复发送DNS请求了。
 * <p>
 * 在java.net.InetAddress类同样采用了这种策略。在默认情况下，会缓存一段有限时间的映射，对于主机名解析不成功的结果，会缓存非常短的时间（10秒）来提高性能。
 * <p>
 * Java提供了InetAddress类来代表IP地址，InetAddress下还有2个子类：
 * Inet4Address、Inet6Address,它们分别代表Internet Protocol version 4（IPv4）地址 和
 * Internet Protocol version 6（IPv6）地址，不过这两个子类不常用，这里也不在赘述。
 * <p>
 * 此外，InetAddress类没有提供构造器，而是提供了如下两个静态方法来获取InetAddress实例：
 * <p>
 * getByName（String host）：根据主机获取对应的InetAddress对象。
 * <p>
 * getByAddress（byte[] addr）：根据原始IP地址来获取对应的InetAddress对象。
 *
 * @author junying.han
 */
public class InetAddressDemo {

    public static void print(InetAddress inetAddress) throws IOException {
        if (inetAddress == null) {
            return;
        }
        String str = inetAddress.toString();
        // 获取以字节数组形式的ip地址, 返回此 InetAddress 对象的原始 IP 地址
        byte[] address = inetAddress.getAddress();
        System.err.println(str + " = " + address);
        // 获取点分四段式ip地址
        String hostAddress = inetAddress.getHostAddress();
        System.err.println("IP地址：" + hostAddress);
        // 获取主机名
        String hostName = inetAddress.getHostName();
        System.err.println("主机名：" + hostName);
        //获取主机名（每次方法都解析DNS去获取主机名，而不从缓存中获取）
        String canonicalHostName = inetAddress.getCanonicalHostName();
        System.err.println("canonicalHostName = " + canonicalHostName);
        // 可以根据指定timeout时间内测试网络是否可达，是否可以建立连接。
        System.err.println("isReachable = " + inetAddress.isReachable(500));
        //可以指定从本机那个网卡、生存时间ttl 和指定时间来测试网络是否可达。
        // public boolean isReachable(NetworkInterface netif, int ttl, int timeout);


        // 当IP地址是通配符地址时返回true，否则返回false。
        // IPv4的通配符地址是0.0.0.0
        // IPv6的通配符地址是0:0:0:0:0:0:0:0，也可以简写成::。
        boolean isAnyLocalAddress = inetAddress.isAnyLocalAddress();
        System.err.println("isAnyLocalAddress =" + isAnyLocalAddress);


        // 当IP地址是本地连接地址（LinkLocalAddress）时返回true，否则返回false。
        // IPv4的本地连接地址的范围是169.254.0.0 ~ 169.254.255.255。
        // IPv6的本地连接地址的前12位是FE8，其他的位可以是任意取值，如FE88::和FE80::ABCD::都是本地连接地址。
        boolean isLinkLocalAddress = inetAddress.isLinkLocalAddress();
        System.err.println("isLinkLocalAddress =" + isLinkLocalAddress);


        // 当IP地址是loopback地址时返回true，否则返回false。loopback地址就是代表本机的IP地址。
        // IPv4的loopback地址的范围是127.0.0.0 ~ 127.255.255.255，也就是说，只要第一个字节是127，就是lookback地址。
        // IPv6的loopback地址是0:0:0:0:0:0:0:1，也可以简写成::1
        boolean isLoopbackAddress = inetAddress.isLoopbackAddress();
        System.err.println("isLoopbackAddress =" + isLoopbackAddress);


        // 当IP地址是全球范围的广播地址时返回true，否则返回false。全球范围的广播地址可以向Internet中的所有的计算机发送信息。
        // IPv4的广播地址除了224.0.0.0和第一个字节是239的IP地址都是全球范围的广播地址
        // IPv6的全球范围的广播地址中第一个字节是FF，第二个字节的范围是0E ~ FE，其他的字节可以是任意值，如FFBE::、FF0E::都是全球范围的广播地址。
        boolean isMCGlobal = inetAddress.isMCGlobal();
        System.err.println("isMCGlobal =" + isMCGlobal);


        // 当IP地址是子网广播地址时返回true，否则返回false。
        // IPv4的子网广播地址的范围是224.0.0.0 ~ 224.0.0.255。
        // IPv6的子网广播地址的第一个字节是FF，第二个字节的范围是02 ~ F2，其他的字节可以是任意值，如FFB2:: 和 FF02:ABCD:: 都是子网广播地址。
        boolean isMCLinkLocal = inetAddress.isMCLinkLocal();
        System.err.println("isMCLinkLocal =" + isMCLinkLocal);


        // 当IP地址是本地接口广播地址时返回true，否则返回false。
        // 本地接口广播地址不能将广播信息发送到产生广播信息的网络接口，即使是同一台计算机的另一个网络接口也不行。
        // 所有的IPv4广播地址都不是本地接口广播地址。IPv6的本地接口广播地址的第一个字节是FF，第二个节字的范围是01 ~ F1，
        // 其他的字节可以是任意值，如FFB1：：、FF01：A123：：都是本地接口广播地址。
        boolean isMCNodeLocal = inetAddress.isMCNodeLocal();
        System.err.println("isMCNodeLocal =" + isMCNodeLocal);


        // 当IP地址是组织范围的广播地址时返回ture，否则返回false。
        // 使用组织范围广播地址可以向公司或企业内部的所有的计算机发送广播信息。
        // IPv4的组织范围广播地址的第一个字节是239，第二个字节不小于192，第三个字节不大于195，如239.193.100.200、239.192.195.0都是组织范围广播地址。
        // IPv6的组织范围广播地址的第一个字节是FF，第二个字节的范围是08 ~ F8，其他的字节可以是任意值，如FF08:: 和 FF48::都是组织范围的广播地址。
        boolean isMCOrgLocal = inetAddress.isMCOrgLocal();
        System.err.println("isMCOrgLocal =" + isMCOrgLocal);


        // 当IP地址是站点范围的广播地址时返回true，否则返回false。
        // 使用站点范围的广播地址，可以向站点范围内的计算机发送广播信息。
        // IPv4的站点范围广播地址的范围是239.255.0.0 ~ 239.255.255.255，如239.255.1.1、239.255.0.0都是站点范围的广播地址。
        // IPv6的站点范围广播地址的第一个字节是FF，第二个字节的范围是05 ~ F5，其他的字节可以是任意值，如FF05:: 和 FF45:: 都是站点范围的广播地址。
        boolean isMCSiteLocal = inetAddress.isMCSiteLocal();
        System.err.println(" isMCSiteLocal=" + isMCSiteLocal);


        // 检查 InetAddress 是否是 IP 多播地址的实用例行程序。
        // 当IP地址是广播地址（MulticastAddress）时返回true，否则返回false。
        // 通过广播地址可以向网络中的所有计算机发送信息，而不是只向一台特定的计算机发送信息。
        // IPv4的广播地址的范围是224.0.0.0 ~ 239.255.255.255
        // IPv6的广播地址第一个字节是FF 其他的字节可以是任意值。
        boolean isMulticastAddress = inetAddress.isMulticastAddress();
        System.err.println("isMulticastAddress =" + isMulticastAddress);


        // 检查 InetAddress 是否是站点本地地址的实用例行程序。
        // 当IP地址是地区本地地址（SiteLocalAddress）时返回true，否则返回false。(是不是内网ip)
        // IPv4的地址本地地址分为三段：10.0.0.0 ~ 10.255.255.255、172.16.0.0 ~ 172.31.255.255、192.168.0.0 ~ 192.168.255.255
        // （企业内部或个人内部的局域网内部的ip都应该在此三个网段内 因为内网用ipv4足够）
        // IPv6的地区本地地址的前12位是FEC，其他的位可以是任意取值，如FED0:: 和 FEF1:: 都是地区本地地址。
        boolean isSiteLocalAddress = inetAddress.isSiteLocalAddress();
        System.err.println("isSiteLocalAddress =" + isSiteLocalAddress);
    }

    public static InetAddress getLocalHostExactAddress() throws SocketException, UnknownHostException {
        InetAddress candidateAddress = null;
        Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
        while (networkInterfaces.hasMoreElements()) {
            NetworkInterface iface = networkInterfaces.nextElement();
            // 该网卡接口下的ip会有多个，也需要一个个的遍历，找到自己所需要的
            for (Enumeration<InetAddress> inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) {
                InetAddress inetAddr = inetAddrs.nextElement();
                // 排除loopback回环类型地址（不管是IPv4还是IPv6 只要是回环地址都会返回true）
                if (inetAddr.isLoopbackAddress()) {
                    continue;
                }
                if (inetAddr.isSiteLocalAddress()) {
                    // 如果是site-local地址，就是它了 就是我们要找的
                    // ~~~~~~~~~~~~~绝大部分情况下都会在此处返回你的ip地址值~~~~~~~~~~~~~
                    return inetAddr;
                }
                // 若不是site-local地址 那就记录下该地址当作候选
                if (candidateAddress == null) {
                    candidateAddress = inetAddr;
                }
            }
        }

        // 如果出去loopback回环地之外无其它地址了，那就回退到原始方案吧
        return candidateAddress == null ? InetAddress.getLocalHost() : candidateAddress;
    }

    /**
     * 获得一个InetAddress对象，该对象含有本地机的域名和IP地址。
     */
    @Test
    public void testLocalHost() throws IOException {
        InetAddress localHost = InetAddress.getLocalHost();
        System.out.println("HostAddress = " + localHost.getHostAddress());
        System.out.println("CanonicalHostName = " + localHost.getCanonicalHostName());
        System.out.println("HostName = " + localHost.getHostName());

        print(localHost);
        boolean isReachable;
        try {
            // 测试是否可以达到该地址。
            isReachable = localHost.isReachable(5);
            System.err.println("isReachable =" + isReachable);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 很多情况是，某一域名对应的主机IP是可能有多个的，我们也可以一一的取回来。
     */
    @Test
    public void testGetAllByName() throws IOException {
        InetAddress[] inetAddressArray = null;
        try {
            inetAddressArray = InetAddress.getAllByName("www.baidu.com");
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        for (InetAddress inetAddress : inetAddressArray) {
            System.err.println("-------------------------------------------");
            print(inetAddress);
            boolean isReachable;
            try {
                isReachable = inetAddress.isReachable(5);
                System.err.println("isReachable =" + isReachable);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 获取回环地址
     */
    @Test
    public void getLoopbackAddress() {
        InetAddress inet = InetAddress.getLoopbackAddress();
        System.out.println("域名：" + inet.getHostName()); // 域名：localhost
        System.out.println("IP地址：" + inet.getHostAddress()); // IP地址：127.0.0.1
    }

    /**
     * 根据主机获取对应的InetAddress对象。 获得一个InetAddress
     * 类的对象，该对象中含有主机的IP地址和域名，该对象用如下格式表示它包含的信息：www.sina.com.cn/202.108.37.40；
     */
    @Test
    public void testGetByAddress() {
        byte[] addr = new byte[]{127, 0, 0, 1};
        InetAddress inetAddress = null;
        try {
            inetAddress = InetAddress.getByAddress(addr);
            print(inetAddress);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        try {
            boolean isReachable = inetAddress.isReachable(5000);
            System.err.println("isReachable =" + isReachable);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.err.println("--------------------------------");
        try {
            // 根据提供的主机名和 IP 地址创建 InetAddress。
            inetAddress = InetAddress.getByAddress("localhost", addr);
            print(inetAddress);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        try {
            boolean isReachable = inetAddress.isReachable(5000);
            System.err.println("isReachable =" + isReachable);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据主机获取对应的InetAddress对象。 获得一个InetAddress
     * 类的对象，该对象中含有主机的IP地址和域名，该对象用如下格式表示它包含的信息：www.sina.com.cn/202.108.37.40；
     */
    @Test
    public void testGetByName() {
        InetAddress inetAddress = null;
        try {
            // 在给定主机名的情况下确定主机的 IP 地址。
            inetAddress = InetAddress.getByName("www.baidu.com");
            print(inetAddress);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        try {
            boolean isReachable = inetAddress.isReachable(5000);
            System.err.println("isReachable =" + isReachable);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("-------------------------------------------");
        try {
            inetAddress = InetAddress.getByName("node2.es");
            print(inetAddress);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        try {
            boolean isReachable = inetAddress.isReachable(5000);
            System.err.println("isReachable =" + isReachable);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("-------------------------------------------");
        try {
            inetAddress = InetAddress.getByName("114.80.138.123");
            print(inetAddress);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        try {
            boolean isReachable = inetAddress.isReachable(5000);
            System.err.println("isReachable =" + isReachable);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 为了更好的解释为何InetAddress#getLocalHost()不靠谱，有必要先普及下Linux下的相关文件的解释。
     * Linux下/sysconfig/network、hosts、host.conf、resolv.conf文件解释。
     * <li>1. /etc/sysconfig/network文件：此文件是针对本计算机的，是给计算机起的一个名字，是计算机的一个标识。可以使用uname -n 命令来查看本地计算机的计算机名称
     * <li>2. /etc/hosts文件: 此文间是在网络上使用的。和Windows系统下的hosts文件相类似，就是一个文本文件，里面存放一些IP和域名的对应关系（注意：都是先ip，后域名）
     * <li>3. /etc/host.conf文件 : 解析顺序/方式，属于一种规则配置
     * <li>4. /etc/resolv.conf文件:DNS服务器的配置文件，用于设置DNS服务器的IP地址及DNS域名，还包含了主机的域名搜索顺序。该文件由域名解析器来读取
     */
    @Test
    public void testGetLocalHost() throws UnknownHostException, SocketException {
        // 在Windows和Linux下，返回值不一样。开一个vpn再运行试一下，也不一样
        // Linux下请你一定不要使用它来获取本机IP，因为它就是简单的读取/etc/hosts的内容，所以它默认返回的是127.0.0.1
        System.err.println("错误示例：" + InetAddress.getLocalHost().getHostAddress());
        System.err.println("正确姿势：" + getLocalHostExactAddress().getHostAddress());

    }

    /**
     * 自己指定DNS地址
     */
    public void setDns() {
        // 这么一来我们的hostName域名都将交给`114.114.114.114`去帮我们完成解析
        System.setProperty("sun.net.spi.nameservice.nameservers", "114.114.114.114");

    }

    /**
     * 自己指定DNS服务的提供者：
     */
    public void setDnsProvider() {
        // 若你没指定此key，那就是default。默认就会使用系统自带的DNS
        // `dns,sun`的意思是：会使用`sun.net.spi.nameservice.nameservers`配置指定的DNS来解析
        System.setProperty("sun.net.spi.nameservice.provider.1", "dns,sun");
    }

    /**
     * 给定主机名，返回其IP地址的数组，基于系统配置的DNS服务解析。当然host可以是主机名(域名)或或者是ip地址，这里以www.baidu.com为例。
     * 注意事项：
     *
     * <li>对于外网域名的解析（如www.baidu.com），你的机器必须能够访问外网才能解析到IP地址。否则java.net.UnknownHostException。
     * 当然若你是在Linux下通过resolv.conf指定了自己的域名解析器，那么到底解析到哪去由你决定（比如你的内网域名都可以被解析了）
     * <li>域名不能加上协议。若你这么写http://www.baidu.com就抛错UnknownHostException
     * <li>对于外网域名解析，每个人解析得到的地址可能不一样。比如此处我对www.baidu.com解析得到的地址是61.135.169.125，是因为我在北京所以得到的是北京的一个IP地址
     *
     * @throws UnknownHostException
     */
    @Test
    public void getAllByName() throws UnknownHostException {
        String host = "www.baidu.com";
        InetAddress[] inets = InetAddress.getAllByName(host);
        Stream.of(inets).forEach(System.out::println);

        System.out.println("------------------------------------");
        Stream.of(InetAddress.getAllByName("xdcs.ximalaya.com"))
                .forEach(System.out::println);
    }

    /**
     * 它的原理是上面的方法 -> InetAddress.getAllByName(host)[0]取值第一个就是它
     *
     * @throws UnknownHostException
     */
    @Test
    public void getByName() throws UnknownHostException {
        // 网络域名
        InetAddress inet = InetAddress.getByName("www.baidu.com");
        System.out.println("域名：" + inet.getHostName()); // 域名：www.baidu.com
        System.out.println("IP地址：" + inet.getHostAddress()); // IP地址：61.135.169.125

        // 本地域名（本机）
        inet = InetAddress.getByName("localhost");
        System.out.println("域名：" + inet.getHostName()); // 域名：localhost
        System.out.println("IP地址：" + inet.getHostAddress()); // IP地址：127.0.0.1

        // 不存在的域名 抛出异常：java.net.UnknownHostException: aaaaaa.com
        // tips：abc.com这种域名是存在的哟
        inet = InetAddress.getByName("aaaaaa.com");
        System.out.println("域名：" + inet.getHostName());
        System.out.println("IP地址：" + inet.getHostAddress());
    }

    /**
     * 根据提供的主机名以及 IP 地址创建InetAddress
     *
     * @throws UnknownHostException
     */
    @Test
    public void getByAddress() throws UnknownHostException {
        // 同时指定域名 和 ip地址，那就是自己建立了对应关系喽
        InetAddress inet = InetAddress.getByAddress("www.baidu.com", new byte[]{61, (byte) 135, (byte) 169, 125});
        System.out.println("域名：" + inet.getHostName()); // 域名：www.baidu.com
        System.out.println("IP地址：" + inet.getHostAddress()); // IP地址：61.135.169.125
    }

    /**
     * @throws UnknownHostException
     */
    @Test
    public void getByAddress2() throws UnknownHostException {
        InetAddress inet = InetAddress.getByAddress(new byte[]{61, (byte) 135, (byte) 169, 125});
        // 若你要获取主机名，就尝试通过网络帮你找，所以一般比较耗时，不建议使用。  找不到就原样输出
        System.out.println("域名：" + inet.getHostName()); // 域名：61.135.169.125
        System.out.println("IP地址：" + inet.getHostAddress()); // IP地址：61.135.169.125
    }


}
